Hyunjung Im

Frontend Developer

github

스코프와 클로저 (정리)

2022-12-24

Scope

  • 변수를 생성하면 변수는 어디에 저장되는가?
  • 프로그램은 어떻게 변수를 찾는가?
  • 식별자에 대한 유효범위
  • 식별자 이름으로 변수를 찾기 위한 규칙의 집합
  • 스코프는 두 가지 방식으로 작동한다.
    1. Lexical Scope : 일반적이고 다수의 프로그래밍 언어가 사용하는 방식
    2. 동적 스코프(Dynamic Scopy)

변수를 검색하는 이유

  • 변수에 값을 대입하거나(LHS 참조) 변수의 값을 얻어오기 위해서(RHS 참조)

컴파일러 이론

  • js는 동적 언어 또는 인터프리터 언어로 분류하지만 사실은 컴파일러 언어이다..?
  • 자바스크립트 엔진이 코드를 인터프리팅하기 전에 컴파일 한다.

컴파일 단계

토크나이징 Tokenizing / 렉싱 Lexing

  • 문자열을 나누어 토큰 이라 불리는 의미 있는 조각으로 만드는 과정.
  • 일반적인 언어의 컴파일러는 첫 단계를 토크나이징 또는 렉싱이라 불리는 작업으로 시작한다.

파싱 Parsing

  • 토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정이다.
  • 파싱의 결과로 만들어진 트리를 AST(Abstract Syntax Tree 추상 구문 트리)라 부른다.

LHS RHS 참조 검색 방식

  • 대입 연산의 방향
  • 모두 현재 실행 중인 스코프에서 시작한다.
  • LHS 검색은 값을 넣어야 하므로 변수 컨테이너 자체를 찾는다.
  • RHS 검색은 단순히 특정 변수의 값을 찾는 것과 다를 바 없다.
  • 어떤 변수의 첫 RHS 검색이 실패하면 다시는 변수를 찾을 수 없다. 이렇게 스코프에서 찾지 못한 변수는 ‘선언되지 않은 변수’라고 한다.
  • 반면에 엔진이 LHS 검색을 수행하여 변수를 찾지 못하고 최상위 층(글로벌 스코프)에 도착할 때 프로그램이 ‘Strict Mode’로 동작하고 있는 것이 아니라면, 글로벌 스코프는 엔진이 검색하는 이름을 가진 새로운 변수를 생성해서 엔진에게 넘겨준다.

중첩 스코프

  • 대상 변수를 현재 스코프에서 찾지 못하면 엔진은 다음 바깥의 스코프로 넘어가는 식으로 변수를 찾거나 글로벌 스코프라 부르는 가장 바깥 스코프에 도달할 때까지 계속한다.

렉시컬 스코프

  • 어떤 함수가 어디서 또는 어떻게 호출되는지에 상관없이 함수의 렉시컬 스코프는 함수가 선언된 위치에 따라 정의된다.

함수 vs 블록 스코프

호이스팅

  • 변수와 함수 선언문 모두 코드가 실제 실행되기 전에 먼저 처리된다.
    • 선언된 위치에서 코드의 꼭대기로 “끌어올려”진다.
  • 선언문을 끌어올리는 동작을 호이스팅 이라고 한다.
  • 스코프별로 작동한다.
  • 먼저 함수가 끌어올려지고 다음으로 변수가 올려진다.

스코프 클로저

  • 클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.
  • 클로저는 렉시컬 스코프에 의존해 코드를 작성한 결과로 그냥 발생한다. 모든 코드에서 클로저는 생성되고 사용된다.
var fn;

function foo() {
  var a = 2;

  function baz() {
    console.log(a);
  }

  fn = baz;
}

function bar() {
  fn();
}

foo();

bar(); // console -> 2
  • 어떤 방식으로 내부 함수를 자신이 속한 렉시컬 스코프 밖으로 수송하든 함수는 처음 선언된 곳의 스코프에 대한 참조를 유지한다. 즉 어디에서 해당 함수를 실행하든 클로저가 작용한다.
function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}

wait("hello");
  • timer함수는 wait() 함수의 스코프에 대한 스코프 클로저를 가지고 있으므로 변수 message에 대한 참조를 유지하고 사용할 수 있다.

  • wait() 실행 1초 후, wait의 내부 스코프는 사라져야 하지만 익명의 함수가 여전히 해당 스코프에 대한 클로저를 가지고 있다.

  • 클로저를 설명하는 가장 흔하고 표준적인 사례 : for 반복문

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

모듈

  • 클로저의 능력을 활용하면서 표면적으로는 콜백과 상관없는 코드 패턴들이 있다. 그중 가장 강력한 패턴이다.
function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log(something);
  }

  function doAnother() {
    console.log(another.join("!"));
  }

  return {
    doSomething: doSomething,
    doAnother: doAnother,
  };
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1!2!3!
  • 이들 모두 CoolModule()의 내부 스코프를 렉시컬 스코프(당연히 클로저도 따라온다)로 가진다.
  • 이 코드와 같은 자바스크립트 패턴을 모듈이라고 부른다.